from functools import cached_property
import os
import posixpath
import subprocess
import git
from jinja2 import Template
import six
import urllib
from cvek.crawler import OpenAnolisCVEClient, OpenEulerCVEClient
from cvek.template import cve_issue_template
from cvek.errors import cvekError
from cvek.gitee import GiteeClient
from pyrpkg.layout import RetiredLayout
from pyrpkg import layout
from pathlib import Path
import yaml


class Commands:
    def __init__(self, config):
        self.path = os.path.abspath(os.getcwd())
        self.layout = layout.build(self.path)
        self._spec = None
        self._rpmdefines = None
        self._version = None
        self._ver = None
        self._repo_name = None
        self._push_url = None
        self._repo = None
        self._branch_remote = None
        self._branch_merge = None
        self._rpm_upstream_branch = None
        self.dist = "cq24"
        # Name of default remote to be used for new clone
        self.default_branch_remote = "origin"
        self.config = config

    @cached_property
    def gitee_client(self):
        return GiteeClient(self.config.get("GITEE_TOKEN"))

    def oe_gitee_issue(self, cveid: str):
        return self.gitee_client.get_issues("src-openeuler", self.repo_name, cveid)

    def oe_gitee_comments(self, issue_number: str):
        return self.gitee_client.get_comments(
            "src-openeuler", self.repo_name, issue_number
        )

    def load_SOURCEINFO(self):
        sourceinfo_filepath = os.path.join(self.path, "SOURCEINFO.yaml")
        text = Path(sourceinfo_filepath).read_text("utf-8")
        try:
            from yaml import CLoader as Loader
        except ImportError:
            from yaml import Loader
        res = yaml.load(text, Loader=Loader)
        self._rpm_upstream_branch = res["upstream"]["branch"]

    @property
    def rpm_upstream_branch(self) -> str:
        if not self._rpm_upstream_branch:
            self.load_SOURCEINFO()
        return self._rpm_upstream_branch

    @property
    def branch_merge(self):
        """This property ensures the branch attribute"""

        if not self._branch_merge:
            self.load_branch_merge()
        return self._branch_merge

    @branch_merge.setter
    def branch_merge(self, value):
        self._branch_merge = value

    def load_branch_merge(self):
        """Find the remote tracking branch from the branch we're on.

        The goal of this function is to catch if we are on a branch we can make
        some assumptions about. If there is no merge point then we raise and
        ask the user to specify.
        """

        if self.dist:
            self._branch_merge = self.dist
            return

        try:
            _ = self.repo
        except cvekError:
            self._branch_merge = self.default_branch_merge()
            return

        try:
            localbranch = self.repo.active_branch.name
        except TypeError as e:
            raise cvekError("Repo in inconsistent state: %s" % e)
        try:
            merge = self.repo.git.config("--get", "branch.%s.merge" % localbranch)
        except git.GitCommandError:
            # Remote branch not defined, use the local name
            self._branch_merge = localbranch
            return

        # Trim off the refs/heads so that we're just working with
        # the branch name
        merge = merge.replace("refs/heads/", "", 1)
        self._branch_merge = merge

    def default_branch_merge(self):
        """Get the default branch used when Git repository is not found."""

        raise cvekError("Unable to find Git repo. Use --release\n")

    @property
    def branch_remote(self):
        """This property ensures the branch_remote attribute"""

        if not self._branch_remote:
            self.load_branch_remote()
        return self._branch_remote

    def load_branch_remote(self):
        """Find the name of remote from branch we're on."""

        try:
            remote = self.repo.git.config(
                "--get", "branch.%s.remote" % self.branch_merge
            )
        except (git.GitCommandError, cvekError) as e:
            remote = self.default_branch_remote

        self._branch_remote = remote

    @property
    def repo(self):
        """This property ensures the repo attribute"""

        if not self._repo:
            self.load_repo()
        return self._repo

    def load_repo(self):
        """Create a repo object from our path"""

        # print("Creating repo object from %s", self.path, file=sys.stderr)
        try:
            self._repo = git.Repo(self.path)
        except (git.InvalidGitRepositoryError, git.NoSuchPathError):
            raise cvekError("%s is not a valid repo" % self.path)

    @property
    def push_url(self):
        """This property ensures the push_url attribute"""

        if not self._push_url:
            self.load_push_url()
        return self._push_url

    def load_push_url(self):
        """Find the pushurl or url of remote of branch we're on."""
        try:
            url = self.repo.git.remote("get-url", "--push", self.branch_remote)
        except git.GitCommandError:
            try:
                url = self.repo.git.config(
                    "--get", "remote.%s.pushurl" % self.branch_remote
                )
            except git.GitCommandError:
                try:
                    url = self.repo.git.config(
                        "--get", "remote.%s.url" % self.branch_remote
                    )
                except git.GitCommandError as e:
                    raise cvekError("Unable to find remote push url: %s" % e)
        if isinstance(url, six.text_type):
            # GitPython >= 1.0 return unicode. It must be encoded to string.
            self._push_url = url
        else:
            self._push_url = url.decode("utf-8")

    @property
    def repo_name(self):
        """Property to get repository name

        .. versionadded:: 1.55
        """
        if not self._repo_name:
            self.load_repo_name()
        return self._repo_name

    @repo_name.setter
    def repo_name(self, name):
        """Set repository name"""
        self._repo_name = name

    def load_repo_name(self):
        """Loads repository name

        .. versionadded:: 1.55
        """

        try:
            if self.push_url:
                parts = urllib.parse.urlparse(self.push_url)

                # FIXME
                # if self.distgit_namespaced:
                #     self._module_name = "/".join(parts.path.split("/")[-2:])
                repo_name = posixpath.basename(parts.path.strip("/"))

                if repo_name.endswith(".git"):
                    repo_name = repo_name[: -len(".git")]
                self._repo_name = repo_name
                return
        except cvekError:
            print("Failed to get repository name from Git url or pushurl")

        self.load_nameverrel()
        if self._package_name_spec:
            self._repo_name = self._package_name_spec
            return

        raise cvekError(
            "Could not find current repository name."
            " Use --name and optional --namespace."
        )

    @property
    def version_release(self) -> str:
        """This property ensures the nvr attribute"""

        if not self._version:
            self.load_version_release()
        return self._version

    def load_version_release(self):
        """This sets the nvr attribute"""
        self._version = "%s-%s" % (self.ver, self._rel[:-5])

    @property
    def ver(self):
        """This property ensures the ver attribute"""
        if not self._ver:
            self.load_nameverrel()
        return self._ver

    def _parse_output_for_nameverrel(self, output):
        """Parses an output of the 'rpm' command - extracts fields from
        the string and set the Command object's properties.
        """
        # Get just the output, then split it by ??, grab the first and split
        # again to get ver and rel
        first_line_output = output.split("??")[1]
        parts = first_line_output.split()
        if len(parts) != 4:
            raise cvekError("Could not get n-v-r-e from %r" % first_line_output)
        (self._package_name_spec, self._epoch, self._ver, self._rel) = parts

        # Most packages don't include a "Epoch: 0" line, in which case RPM
        # returns '(none)'
        if self._epoch == "(none)":
            self._epoch = "0"

    def load_nameverrel(self):
        """Set the release and version of a package."""

        #  Otherwise, we get 'verrel' information from the '.spec' file.
        cmd = ["rpm"]
        cmd.extend(self.rpmdefines)

        specfile_path = os.path.join(self.layout.specdir, self.spec)

        # We make sure there is a space at the end of our query so that
        # we can split it later.  When there are subpackages, we get a
        # listing for each subpackage.  We only care about the first.
        cmd.extend(
            [
                "-q",
                "--qf",
                "??%{NAME} %{EPOCH} %{VERSION} %{RELEASE}??",
                "--specfile",
                "%s" % specfile_path,
            ]
        )
        joined_cmd = " ".join(cmd)
        try:
            proc = subprocess.Popen(
                cmd,
                universal_newlines=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            output, err = proc.communicate()
        except Exception as e:
            print(
                "Errors occoured while running following command to get N-V-R-E:"
            )
            print(joined_cmd)
            raise cvekError("Could not query n-v-r: %s" % e)
        if err:
            print(
                "Errors occoured while running following command to get N-V-R-E:"
            )
            print(joined_cmd)
            print(err)
        if proc.returncode > 0:
            raise cvekError(
                "Could not get n-v-r-e from %s"
                % os.path.join(self.layout.specdir, self.spec)
            )

        self._parse_output_for_nameverrel(output)

    @property
    def rpmdefines(self):
        """This property ensures the rpm defines"""

        if not self._rpmdefines:
            self.load_rpmdefines()
        return self._rpmdefines

    def load_rpmdefines(self):
        """Populate rpmdefines based on current active branch"""

        if self.layout is None or isinstance(self.layout, RetiredLayout):
            raise cvekError("Unexpected source package layout.")

        # Fixme: hardcode
        self._distvar = "cqos"
        self._distval = "24"
        self._disttag = "cq%s" % self._distval
        self._rpmdefines = [
            "--define",
            "_sourcedir %s" % self.layout.sourcedir,
            "--define",
            "_specdir %s" % self.layout.specdir,
            "--define",
            "_builddir %s" % self.layout.builddir,
            "--define",
            "_srcrpmdir %s" % self.layout.srcrpmdir,
            "--define",
            "_rpmdir %s" % self.layout.rpmdir,
            "--define",
            "_rpmfilename %s" % self.layout.rpmfilename,
            "--define",
            "dist .%s" % self._disttag,
            "--define",
            "%s %s" % (self._distvar, self._distval.split("_")[0]),
            # int and float this to remove the decimal
            "--define",
            "%s 1" % self._disttag,
        ]

    @property
    def spec(self):
        """This property ensures the spec attribute"""

        if not self._spec:
            self.load_spec()
        return self._spec

    def load_spec(self):
        """This sets the spec attribute"""

        if self.layout is None or isinstance(self.layout, layout.IncompleteLayout):
            raise cvekError("Spec file is not available")
        if isinstance(self.layout, layout.RetiredLayout):
            raise cvekError(
                "This package or module is retired. The action has stopped."
            )

        # Get a list of ".spec" files in the path we're looking at
        specs = [
            f
            for f in os.listdir(self.layout.specdir)
            if f.endswith(".spec") and not f.startswith(".")
        ]

        if specs:
            # Prefer the spec matching the directory name
            self._spec = os.path.basename(self.layout.root_dir) + ".spec"
            if specs != [self._spec]:
                if self._spec not in specs:
                    self._spec = specs[0]
                # Warn if more or less than the spec matching the directory name was found
                self.log.warning("Using {0}".format(self._spec))
            return

        raise cvekError("No spec file found.")

    def clog(self, raw=False, subject=None):
        """Write the latest spec changelog entry to a clog file"""

        spec_file = os.path.join(self.layout.specdir, self.spec)
        # TODO: remove when fixed
        # Command contains workaround (undefines _changelog_trimtime) described at:
        # https://github.com/rpm-software-management/rpm/issues/1301
        # It caused, that older changelog records were not displayed.
        #
        # [%{CHANGELOGTEXT}] is an array that contains all changelog records in a specfile
        # <SEPARATOR> tag is placed between changelogs records to be able to extract the first one
        cmd = (
            ["rpm"]
            + self.rpmdefines
            + [
                "-q",
                "--qf",
                "[%{CHANGELOGTEXT}\n<SEPARATOR>]",
                "--undefine",
                "_changelog_trimtime",
                "--specfile",
                "%s" % spec_file,
            ]
        )
        proc = subprocess.Popen(
            cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True
        )
        stdout, stderr = proc.communicate()
        if proc.returncode > 0:
            raise cvekError(stderr.strip())

        firstline = True
        if subject:
            clog_lines = [subject + "\n", "\n"]
        else:
            clog_lines = []
        buf = six.StringIO(stdout)
        for line in buf:
            if line.startswith("<SEPARATOR>"):
                break
            if line == "\n" or line.startswith("$"):
                continue
            if line == "(none)\n":
                # (none) may appear as the last line in changelog got from SPEC
                # file. In some cases, e.g. there is only one changelog entry
                # in SPEC, no (none) line presents. Thus, when for loop ends, all
                # lines of changelog are handled.
                break
            if raw:
                clog_lines.append(line)
            else:
                clog_lines.append(line.replace("- ", "", 1))
            if firstline and not subject:
                # Add a newline after the first line to ensure that Git doesn't
                # concatenate them all as the subject.
                clog_lines.append("\n")
                firstline = False

        buf.close()

        # Now open the clog file and write out the lines
        with open(os.path.join(self.path, "clog"), "w") as clog:
            clog.writelines(clog_lines)

    def generate_issue(self, cve_id):
        oe = OpenEulerCVEClient()
        oe_data = oe.get_data(cve_id, self.repo_name)

        oa = OpenAnolisCVEClient()
        oa_data = oa.get_data(cve_id, self.repo_name)

        _gitee_issue = self.oe_gitee_issue(cve_id)
        # assert len(_gitee_issue) == 1, "Gitee number of issues should be 1"

        html_url = _gitee_issue[0]["html_url"]
        issue_number = _gitee_issue[0]["number"]

        comments = self.oe_gitee_comments(issue_number)
        comments = [c["body"] for c in comments if c["user"]["id"] != 5329419]

        data = {
            "rpm_name": self.repo_name,
            "rpm_verrel": self.version_release,
            "rpm_upstream_branch": self.rpm_upstream_branch,
            "cvss_score": oe_data["cvsssCoreOE"],
            "summary": oe_data["summary"],
            "references": [
                {
                    "name": "NVD官方",
                    "url": f"https://nvd.nist.gov/vuln/detail/{cve_id}",
                },
                {
                    "name": "欧拉社区issue",
                    "url": f"https://gitee.com/src-openeuler/{self.repo_name}/issues?q=is%3Aall+{cve_id}",
                },
                {
                    "name": "CVE官方",
                    "url": f"https://www.cve.org/CVERecord/SearchResults?query={cve_id}",
                },
                {
                    "name": "debian官方",
                    "url": f"https://security-tracker.debian.org/tracker/{cve_id}",
                },
                {
                    "name": "Snyk官方",
                    "url": f"https://security.snyk.io/vuln/?search={cve_id}",
                },
                {
                    "name": "欧拉社区官方",
                    "url": f"https://www.openeuler.org/zh/security/cve/detail/?cveId={cve_id}&packageName={self.repo_name}",
                },
                {
                    "name": "龙蜥社区官方",
                    "url": f"https://anas.openanolis.cn/cves/detail/{cve_id}",
                },
            ],
            "cve_sources": [
                oe_data,
                oa_data,
            ],
            "html_url": html_url,
            "comments": comments,
        }

        issue_template = Template(cve_issue_template)
        issue_text = issue_template.render(data)

        for filename in os.listdir("cve"):
            if filename.startswith(f"{cve_id}"):
                cve_filepath = os.path.join("cve", filename)
                break

        # 读取文件内容
        try:
            with open(cve_filepath, "r", encoding="utf-8") as file:
                content = file.read()
        except Exception as e:
            print(f"Failed to read file {cve_filepath}: {e}")

        metadata_end = content.find("---", 3)
        if metadata_end != -1:
            metadata = content[3:metadata_end].strip()
            content_lines = metadata.split("\n")

            issue_number = None
            assignee = None
            tags = []

            for line in content_lines:
                if line.startswith("issue_number:"):
                    issue_number = int(line.split(":", 1)[1].strip())
                elif line.startswith("assignee:"):
                    assignee = line.split(":", 1)[1].strip()
                elif line.startswith("tags:"):
                    tags = [
                        tag.strip()
                        for tag in line.split(":", 1)[1].strip().split(",")
                        if tag.strip()
                    ]

            if issue_number is None:
                print(f"No issue_number found in {cve_filepath}")
                exit(1)

        try:
            with open(cve_filepath, "w", encoding="utf-8") as file:
                file.write(f"---\nissue_number: {issue_number}\n")
                file.write(f"tags: {', '.join(tags) if tags else ''}\n")
                file.write(f"assignee: {assignee}\n---\n\n")
                file.write(issue_text + "\n")
        except Exception as e:
            print(f"Failed to write file {filename}: {e}")

        # print(issue_text)
